package com.liwy3.seckill.controller;

import com.liwy3.common.domain.R;
import com.liwy3.seckill.entity.SeckillEntity;
import com.liwy3.seckill.service.SeckillService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Copyright © 元气森林（北京）食品科技集团有限公司. All rights reserved.
 *
 * @Description: 秒杀程序实现
 * @author: Crazy4J
 * @date: 2022年03月10日 16:03
 * @version: V1.0
 */
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/seckill")
public class SeckillController {

    private final SeckillService seckillService;
    private final ExecutorService executorService = Executors.newFixedThreadPool(10);
    //模拟1000个参与秒杀得用户 => 如果参与秒杀用户比较少(比如用户120:商品100),会出现商品剩余 =>原因:乐观锁多线程会存在更新库存失败
    private final int seckillNum = 1000;

    private final RedissonClient redissonClient;
    private final DataSourceTransactionManager dataSourceTransactionManager;
    private final TransactionDefinition transactionDefinition;

    /**
     * 秒杀实现方式一 : 乐观锁实现方式
     */
    @GetMapping("/optimisticLock")
    public R<?> OptimisticLock(String seckillId) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(seckillNum);
        AtomicInteger successCount = new AtomicInteger();

        //开启1000个秒杀线程
        for (int i = 1; i <= seckillNum; i++) {
            executorService.execute(new OptimisticWorker(countDownLatch, seckillId, successCount, i));
        }
        countDownLatch.await();
        log.info("秒杀成功总数:{}", successCount.get());
        return R.ok();
    }

    /**
     * 秒杀实现方式二 : redis实现  如果使用事务, 需要保证先提交事务, 再释放锁(手动处理事务比较合适)
     *                          可以利用AOP + redis锁实现
     * spring注解事务@Transactional和分布式锁一起使用的问题
     *
     * 这是因为@Transactional是通过方法是否抛出异常来判断事务是否回滚还是提交，此时方法已经结束。
     * 但是我们必须在方法结束之后释放锁，因此在释放锁之后，此时事务还没提交，由于锁已经释放，其他进程可以获得锁，
     * 并从数据库查询数据，但是此时前一个进程没有提交数据。该进程查到的数据不是最新的数据。
     * 尽管这个过程只要很短的时间（我实际测试过程中这个过程只要几毫秒），但是高并发的情况还是会出问题。
     *
     */
    @GetMapping("/redisLock")
    public R<?> redisLock(String seckillId) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(seckillNum);
        AtomicInteger successCount = new AtomicInteger();

        for (int i = 1; i <= seckillNum; i++) {
            executorService.execute(new RedisWorker(countDownLatch, seckillId, successCount, i));
        }
        countDownLatch.await();
        log.info("秒杀成功总数:{}", successCount.get());
        return R.ok();
    }





    class RedisWorker implements Runnable {
        private final CountDownLatch countDownLatch;
        private final String seckillId;
        private final Integer userId;
        private final AtomicInteger successCount;

        public RedisWorker(CountDownLatch countDownLatch, String seckillId, AtomicInteger successCount, Integer userId) {
            this.countDownLatch = countDownLatch;
            this.seckillId = seckillId;
            this.successCount = successCount;
            this.userId = userId;
        }

        @Override
        public void run() {
            RLock lock = redissonClient.getLock("seckillId:" + seckillId);
            boolean flag = false;
            TransactionStatus transactionStatus = null;
            try {
                flag = lock.tryLock(3, 10, TimeUnit.SECONDS);
                if (flag) {
                    //如果需要开启事务时, 需要手动开启事务(此处只是模拟)
                    transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
                    SeckillEntity seckillEntity = seckillService.lambdaQuery().eq(SeckillEntity::getSeckillId, seckillId).one();
                    if (seckillEntity.getNumber() > 0) {
                        seckillService.lambdaUpdate().setSql("number = number - 1")
                                .eq(SeckillEntity::getSeckillId, seckillId).update();
                        successCount.incrementAndGet();
                        log.info("秒杀成功" + userId);
                    } else {
                        log.info("秒杀失败" + userId);
                    }
                    dataSourceTransactionManager.commit(transactionStatus);
                } else {
                    log.info("秒杀失败" + userId);
                }
                countDownLatch.countDown();
            } catch (Exception e) {
                if (transactionStatus != null) {
                    dataSourceTransactionManager.rollback(transactionStatus);
                }
                throw new RuntimeException(e);
            } finally {
                if (flag) {
                    lock.unlock();
                }
            }
        }
    }


    class OptimisticWorker implements Runnable {
        private final CountDownLatch countDownLatch;
        private final String seckillId;
        private final Integer userId;
        private final AtomicInteger successCount;

        public OptimisticWorker(CountDownLatch countDownLatch, String seckillId, AtomicInteger successCount, Integer userId) {
            this.countDownLatch = countDownLatch;
            this.seckillId = seckillId;
            this.successCount = successCount;
            this.userId = userId;
        }

        @Override
        public void run() {
            SeckillEntity seckillEntity = seckillService.lambdaQuery().eq(SeckillEntity::getSeckillId, seckillId).one();
            if (seckillEntity.getNumber() > 0) {
                boolean update = seckillService.lambdaUpdate().setSql("number = number - 1, version = version + 1")
                        .eq(SeckillEntity::getSeckillId, seckillId).eq(SeckillEntity::getVersion, seckillEntity.getVersion()).update();
                if (update) {
                    successCount.incrementAndGet();
                    log.info("秒杀成功" + userId);
                } else {
                    log.info("秒杀失败" + userId);
                }
            }
            log.info("处理第{}条数据", userId);
            countDownLatch.countDown();
        }
    }


}
